/**
 *  Serponix is an arcade game in focus to multiplayer based on the classic game Snake.
 *  Copyright (C) 2010 - 2011  Daniel Vala
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License,
 *  or  any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.

 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 *  If you have any question do not hesitate to contact author
 *  on e-mail address: danielvala42@gmail.com
 */
package com.serponix.net;

import com.serponix.game.Direction;
import com.serponix.game.GameModeEnum;
import com.serponix.game.Mapa;
import com.serponix.game.ModelClient;
import com.serponix.game.Policko;
import com.serponix.gui.lobby.ModelLobbyClient;
import com.serponix.settings.GameSettings;
import java.awt.Color;
import java.awt.Dimension;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;

/**
 * Obsluhuje výměnu dat se serverem
 *
 * @author Daniel Vala
 */
public class UDPClient implements Client {

	//  private LobbyProClienta lobbyProClienta;
	private ModelLobbyClient modelLobbyClient;
	private DatagramSocket socket;
	private ModelClient model;
	private InetAddress serverIP;
	private boolean prijemLobby;
	private boolean cancel;
	private final int PORT;

	public UDPClient() {
		PORT = GameSettings.getInstance().getPort();
		prijemLobby = true;
		try {
			socket = new DatagramSocket();
		} catch (SocketException ex) {
			System.exit(1);
		}
	}

	@Override
	public void setLobby(ModelLobbyClient modelLobbyClient) {
		this.modelLobbyClient = modelLobbyClient;
	}

	@Override
	public void setModel(ModelClient model) {
		this.model = model;
	}

	@Override
	public InetAddress getServerIP() {
		return serverIP;
	}

	@Override
	public void odpojSe() {
		poslatData(Informace.KONEC + Informace.ODDELOVAC1);
		cancel = true;
		socket.close();
	}

	@Override
	public void run() {
		prijemStavuLobby();
		prijemDat();
	}

	/**
	 * přijímá stav lobby od serveru
	 */
	@Override
	public void prijemStavuLobby() {
		byte[] data = new byte[20000];
		while (prijemLobby) {

			DatagramPacket packet = new DatagramPacket(data, data.length);
			try {
				socket.receive(packet);
			} catch (SocketException ex) {
				if (!cancel) {
					JOptionPane.showMessageDialog(null, "Neočekávané uzavření socketu.", "Chyba spojení", JOptionPane.ERROR_MESSAGE);
				}
				return;
			} catch (IOException ex) {
				JOptionPane.showMessageDialog(null, "Chyba při čtení ze sítě.", "Chyba spojení", JOptionPane.ERROR_MESSAGE);
			}

			String prijataZprava;
			try {
				prijataZprava = new String(packet.getData(), 0, packet.getLength(), "UTF-8");
			} catch (UnsupportedEncodingException ex) {
				throw new RuntimeException(ex);
			}
			StringTokenizer st = new StringTokenizer(prijataZprava, Informace.ODDELOVAC1);
			int druhZpravy = 0;
			try {
				druhZpravy = Integer.parseInt(st.nextToken());
			} catch (NumberFormatException ex) {
				System.err.println("Příjem nerozeznatelného pokynu od serveru:");
				System.err.println(prijataZprava);
			}

			switch (druhZpravy) {
				case Informace.START:
					int pocetHracu = 0;
					Dimension velikostHerniPlochy = null;
					Mapa mapa = new Mapa();
					String podZprava = st.nextToken();
					st = new StringTokenizer(podZprava, Informace.ODDELOVAC2);
					while (st.hasMoreTokens()) {
						String atributNastaveni = st.nextToken();
						StringTokenizer st2 = new StringTokenizer(atributNastaveni, Informace.ODDELOVAC3);
						int typ = Integer.parseInt(st2.nextToken());
						switch (typ) {
							case Informace.POCET_HRACU:
								pocetHracu = Integer.parseInt(st2.nextToken());
								break;
							case Informace.VELIKOST_HERNI_PLOCHY:
								velikostHerniPlochy = new Dimension(Integer.parseInt(st2.nextToken()), Integer.parseInt(st2.nextToken()));
								break;
							case Informace.MAPA:
								mapa = new Mapa(st2.nextToken());
								break;
							default:
								st2.nextToken();
						}
					}
					modelLobbyClient.startHry(pocetHracu, velikostHerniPlochy, mapa);
					prijemLobby = false;
					break;
				case Informace.ZPRAVA:
					String informace = st.nextToken();
					zobrazInformaci(informace);
					break;
				case Informace.HRAC:
					String podZprava2 = st.nextToken();
					st = new StringTokenizer(podZprava2, Informace.ODDELOVAC3);
					int cisloHrace = Integer.parseInt(st.nextToken());
					String jmeno = st.nextToken();
					modelLobbyClient.setHrac(cisloHrace, jmeno);
					break;
				case Informace.NAZEV_MAPY:
					modelLobbyClient.setStitekMapy(st.nextToken());
					break;
				case Informace.GAME_MODE:
					modelLobbyClient.setGameMode(GameModeEnum.valueOf(st.nextToken()));
					break;
				case Informace.RYCHLOST:
					modelLobbyClient.setRychlostHry(st.nextToken());
					break;
				case Informace.KONEC:
					JOptionPane.showMessageDialog(null, "Server ukončil hru.", "Zpráva", JOptionPane.INFORMATION_MESSAGE);
					modelLobbyClient.konec();
					break;
				case Informace.VYHODIT:
					JOptionPane.showMessageDialog(null, "Byl jste vyhozen ze hry.", "Zpráva", JOptionPane.INFORMATION_MESSAGE);
					modelLobbyClient.konec();
					break;
				default:
					System.err.println("Příjem nerozeznatelného pokynu od serveru:");
					System.err.println(prijataZprava);
			}
		}
	}

	/**
	 * přijímá stav hry, články hadů a jejich střel, všechny štítky a zprávy
	 */
	@Override
	public void prijemDat() {
		while (true) {
			byte[] data = new byte[20000];
			DatagramPacket packet = new DatagramPacket(data, data.length);
			try {
				socket.receive(packet);
			} catch (SocketException ex) {
				if (!cancel) {
					JOptionPane.showMessageDialog(null, "Neočekávané uzavření socketu.", "Chyba spojení", JOptionPane.ERROR_MESSAGE);
					model.ukonciHru();
				}
				return;
			} catch (IOException e) {
				zobrazInformaci(e.toString());
			}
			String prijataZprava;
			try {
				prijataZprava = new String(packet.getData(), 0, packet.getLength(), "UTF-8");
			} catch (UnsupportedEncodingException ex) {
				throw new RuntimeException(ex);
			}
			StringTokenizer st = new StringTokenizer(prijataZprava, Informace.ODDELOVAC1);
			int druhZpravy = 0;
			try {
				druhZpravy = Integer.parseInt(st.nextToken());
			} catch (NumberFormatException ex) {
				System.err.println("Příjem nerozeznatelného pokynu od serveru.");
			}

			switch (druhZpravy) {
				case Informace.POLICKO:
					//    model.smazVsechnyPolicka();
					String podZprava = st.nextToken();
					st = new StringTokenizer(podZprava, Informace.ODDELOVAC2);
					// přečtení a přidání všech políček
					while (st.hasMoreTokens()) {
						String policko = st.nextToken();
						StringTokenizer st2 = new StringTokenizer(policko, Informace.ODDELOVAC3);

						int typ = Integer.parseInt(st2.nextToken());
						int x = Integer.parseInt(st2.nextToken());
						int y = Integer.parseInt(st2.nextToken());
						Color barva = null;
						int informace = 0;
						Direction smer = null;
						boolean neviditelny = false;
						if (st2.hasMoreTokens()) {
							barva = Color.decode(st2.nextToken());
						}
						if (st2.hasMoreTokens()) {
							informace = Integer.parseInt(st2.nextToken());
						}
						if (st2.hasMoreTokens()) {
							int a = Integer.parseInt(st2.nextToken());
							smer = Direction.prevedCisloNaSmer(a);
						}
						if (st2.hasMoreTokens()) {
							if (Integer.parseInt(st2.nextToken()) == Informace.NEVIDITELNOST) {
								neviditelny = true;
							}
						}
						Policko p = new Policko(typ, x, y, barva, informace, smer, neviditelny);

						model.pridejPolicko(p);
					}
					break;
				case Informace.SKORE:
					String skore = st.nextToken();
					st = new StringTokenizer(skore, Informace.ODDELOVAC3);
					int i = 0;
					while (st.hasMoreTokens()) {
						String text = st.nextToken();
						model.setAllSkoreText(i, text);
						i++;
					}
					break;
				case Informace.INFO:
					String informace = st.nextToken();
					model.getPanelZprav().addMessage(informace);
					break;
				case Informace.KROK:
					model.krok();
					break;
				case Informace.VITEZSTVI:
					JOptionPane.showMessageDialog(null, st.nextToken(), "KONEC HRY", JOptionPane.INFORMATION_MESSAGE);
					model.ukonciHru();
					break;
				case Informace.KONEC:
					JOptionPane.showMessageDialog(null, "Server ukončil hru.", "KONEC HRY", JOptionPane.INFORMATION_MESSAGE);
					model.ukonciHru();
					break;
				default:
					System.err.println("Příjem nerozeznatelného pokynu od serveru:");
					System.err.println(prijataZprava);
			}
		}
	}

	/**
	 * vyhledá hry na některých IP adresách
	 *
	 * @return seznam IP adres, na kterých jsou založeny hry
	 * @throws java.io.IOException
	 */
	@Override
	public List<InetAddress> vyhledatHry() throws IOException {
		String message = Informace.HLEDANI + Informace.ODDELOVAC1;

		int a = 192;
		int b = 168;
		int c = 1;
		int d = 0;
		byte[] data = message.getBytes();

		while (d != 255) {
			socket.send(new DatagramPacket(data, data.length, InetAddress.getByName(a + "." + b + "." + c + "." + d), PORT));
			d++;
		}

		socket.send(new DatagramPacket(data, data.length, InetAddress.getByName("localhost"), PORT));

		DatagramPacket packet = new DatagramPacket(data, data.length);
		List<InetAddress> nalezeneAdresy = new ArrayList<InetAddress>();
		for (int i = 0; i < 100; i++) {
			try {
				socket.setSoTimeout(1000);
				socket.receive(packet);
			} catch (SocketTimeoutException ex) {
				return nalezeneAdresy;
			}
			String prijataZprava = new String(packet.getData(), 0, packet.getLength(), "UTF-8");
			if (prijataZprava.startsWith(Informace.PRIJEM + Informace.ODDELOVAC1)) {
				nalezeneAdresy.add(packet.getAddress());
			}
		}
		return nalezeneAdresy;
	}

	/**
	 * Připojí se k serveru podle zadané IP adresy
	 *
	 * @param jmenoHrace jméno hráče
	 * @param serverIP   IP adresa serveru
	 * @throws java.net.SocketTimeoutException
	 *
	 * @throws java.io.IOException
	 */
	@Override
	public boolean pripojitSeKServeru(InetAddress serverIP) throws SocketTimeoutException, IOException {
		String message = Informace.NOVY_HRAC + Informace.ODDELOVAC1 + GameSettings.getInstance().getNameOfGivenPlayer(0);
		byte[] data = message.getBytes();
		socket.send(new DatagramPacket(data, data.length, serverIP, PORT));
		byte[] data2 = new byte[2000];
		DatagramPacket packet = new DatagramPacket(data2, data2.length);
		socket.setSoTimeout(1000); // pokud server neodpoví za 1 vteřinu, vyhodí výjimku
		socket.receive(packet);
		String prijataZprava = new String(packet.getData(), 0, packet.getLength(), "UTF-8");
		boolean cislo = true;
		int info = -1;
		try {
			info = Integer.parseInt(prijataZprava);
		} catch (NumberFormatException ex) {
			cislo = false;
		}
		if (cislo) {
			if (info == Informace.PLNY_SERVER) {
				return false;
			}
		}
		socket.setSoTimeout(0); // nastavení timeoutu na nekonečno
		this.serverIP = serverIP;
		return true;
	}

	/**
	 * pošle data serveru
	 *
	 * @param dataKPoslani
	 */
	@Override
	public void poslatData(String dataKPoslani) {
		byte[] data;
		try {
			data = dataKPoslani.getBytes("UTF-8");
		} catch (UnsupportedEncodingException ex) {
			throw new RuntimeException(ex);
		}
		try {
			socket.send(new DatagramPacket(data, data.length, serverIP, PORT));
		} catch (IOException ex) {
			System.err.println("chyba poslání směru");
		}
	}

	/**
	 * zobrazí informaci buď v lobby, nebo ve hře
	 *
	 * @param messageToDisplay
	 */
	private void zobrazInformaci(final String messageToDisplay) {
		SwingUtilities.invokeLater(new Runnable() {

			@Override
			public void run() {
				if (model != null) {
					model.getPanelZprav().addMessage(String.format(messageToDisplay));
				} else {
					modelLobbyClient.zobrazZpravu(messageToDisplay);
				}
			}
		});
	}
}
